package cn.sciento.starter.social.core.security;

import java.util.List;
import java.util.Set;
import java.util.UUID;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import cn.sciento.core.util.TokenUtils;
import cn.sciento.starter.social.core.common.connect.SocialUserData;
import cn.sciento.starter.social.core.common.constant.ChannelEnum;
import cn.sciento.starter.social.core.exception.RejectAuthorizationException;
import cn.sciento.starter.social.core.exception.UserBindException;
import cn.sciento.starter.social.core.exception.UserUnbindException;
import cn.sciento.starter.social.core.provider.Provider;
import cn.sciento.starter.social.core.provider.SocialProviderRepository;
import cn.sciento.starter.social.core.provider.SocialUserProviderRepository;
import cn.sciento.starter.social.core.security.holder.SocialSessionHolder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.authentication.AbstractAuthenticationTargetUrlRequestHandler;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.social.connect.Connection;
import org.springframework.social.connect.ConnectionData;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.social.security.SocialAuthenticationFailureHandler;
import org.springframework.social.security.SocialAuthenticationRedirectException;
import org.springframework.social.security.SocialAuthenticationServiceLocator;
import org.springframework.social.security.SocialAuthenticationToken;
import org.springframework.social.security.provider.SocialAuthenticationService;
import org.springframework.util.Assert;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;

public class SocialAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
  private static final Logger LOGGER = LoggerFactory.getLogger(SocialAuthenticationFilter.class);
  
  private static final String DEFAULT_FAILURE_URL = "/login";
  
  private static final String DEFAULT_FILTER_PROCESSES_URL = "/open";
  
  private static final String DEFAULT_BIND_URL = "/bind";
  
  private String bindUrl = "/bind";
  
  private boolean attemptBind = false;
  
  private boolean updateBind = true;
  
  private boolean enableHttps = false;
  
  private String filterProcessesUrl = "/open";
  
  private TokenStore tokenStore;
  
  private SocialAuthenticationServiceLocator authServiceLocator;
  
  private SimpleUrlAuthenticationFailureHandler delegateAuthenticationFailureHandler;
  
  private SocialUserProviderRepository userProviderRepository;
  
  private SocialProviderRepository socialProviderRepository;
  
  private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
  
  public SocialAuthenticationFilter(AuthenticationManager authManager, TokenStore tokenStore, SocialUserProviderRepository userProviderRepository, SocialAuthenticationServiceLocator authServiceLocator, SocialProviderRepository socialProviderRepository) {
    super("/open");
    setAuthenticationManager(authManager);
    this.tokenStore = tokenStore;
    this.authServiceLocator = authServiceLocator;
    this.socialProviderRepository = socialProviderRepository;
    this.userProviderRepository = userProviderRepository;
    this.delegateAuthenticationFailureHandler = new SimpleUrlAuthenticationFailureHandler("/login");
    setAuthenticationFailureHandler((AuthenticationFailureHandler)new SocialAuthenticationFailureHandler((AuthenticationFailureHandler)this.delegateAuthenticationFailureHandler));
  }
  
  public void setBindUrl(String bindUrl) {
    this.bindUrl = bindUrl;
  }
  
  public void setAttemptBind(boolean attemptBind) {
    this.attemptBind = attemptBind;
  }
  
  public void setUpdateBind(boolean updateBind) {
    this.updateBind = updateBind;
  }
  
  public void setEnableHttps(boolean enableHttps) {
    this.enableHttps = enableHttps;
  }
  
  public void setDefaultFailureUrl(String defaultFailureUrl) {
    this.delegateAuthenticationFailureHandler.setDefaultFailureUrl(defaultFailureUrl);
  }
  
  public void setPostLoginUrl(String postLoginUrl) {
    AuthenticationSuccessHandler successHandler = getSuccessHandler();
    if (successHandler instanceof AbstractAuthenticationTargetUrlRequestHandler) {
      AbstractAuthenticationTargetUrlRequestHandler h = (AbstractAuthenticationTargetUrlRequestHandler)successHandler;
      h.setDefaultTargetUrl(postLoginUrl);
    } else {
      throw new IllegalStateException("can't set postLoginUrl on unknown successHandler, type is " + successHandler.getClass().getName());
    } 
  }
  
  public void setAlwaysUsePostLoginUrl(boolean alwaysUsePostLoginUrl) {
    AuthenticationSuccessHandler successHandler = getSuccessHandler();
    if (successHandler instanceof AbstractAuthenticationTargetUrlRequestHandler) {
      AbstractAuthenticationTargetUrlRequestHandler h = (AbstractAuthenticationTargetUrlRequestHandler)successHandler;
      h.setAlwaysUseDefaultTargetUrl(alwaysUsePostLoginUrl);
    } else {
      throw new IllegalStateException("can't set alwaysUsePostLoginUrl on unknown successHandler, type is " + successHandler.getClass().getName());
    } 
  }
  
  public void setPostFailureUrl(String postFailureUrl) {
    AuthenticationFailureHandler failureHandler = getFailureHandler();
    if (failureHandler instanceof SimpleUrlAuthenticationFailureHandler) {
      SimpleUrlAuthenticationFailureHandler h = (SimpleUrlAuthenticationFailureHandler)failureHandler;
      h.setDefaultFailureUrl(postFailureUrl);
    } else {
      throw new IllegalStateException("can't set postFailureUrl on unknown failureHandler, type is " + failureHandler.getClass().getName());
    } 
  }
  
  public void setSessionStrategy(SessionStrategy sessionStrategy) {
    this.sessionStrategy = sessionStrategy;
  }
  
  public SocialProviderRepository getSocialProviderRepository() {
    return this.socialProviderRepository;
  }
  
  public SocialAuthenticationServiceLocator getAuthServiceLocator() {
    return this.authServiceLocator;
  }
  
  protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
    String providerId = getRequestedProviderId(request);
    if (providerId == null)
      return false; 
    Set<String> authProviders = getAuthServiceLocator().registeredAuthenticationProviderIds();
    return (!authProviders.isEmpty() && authProviders.contains(providerId));
  }
  
  public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
    if (detectRejection(request)) {
      if (this.logger.isDebugEnabled())
        this.logger.debug("A rejection was detected. Failing authentication."); 
      throw new RejectAuthorizationException("Authentication failed because user rejected authorization.");
    } 
    String providerId = getRequestedProviderId(request);
    SocialAuthenticationService<?> authService = getAuthServiceLocator().getAuthenticationService(providerId);
    Authentication auth = attemptAuthService(authService, request, response);
    if (auth == null)
      throw new AuthenticationServiceException("authentication failed"); 
    return auth;
  }
  
  public void setFilterProcessesUrl(String filterProcessesUrl) {
    super.setFilterProcessesUrl(filterProcessesUrl);
    this.filterProcessesUrl = filterProcessesUrl;
  }
  
  protected boolean detectRejection(HttpServletRequest request) {
    String uri = request.getRequestURI();
    Set<?> parameterKeys = request.getParameterMap().keySet();
    if (uri.contains("callback"))
      return (parameterKeys.size() > 0 && 
        !parameterKeys.contains("oauth_token") && 
        !parameterKeys.contains("code") && 
        !parameterKeys.contains("scope")); 
    return false;
  }
  
  protected Authentication attemptAuthService(SocialAuthenticationService<?> authService, HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
    if (!callbackRequest(request)) {
      String state = UUID.randomUUID().toString();
      request.setAttribute("state", state);
      String accessToken = TokenUtils.getToken(request);
      if (StringUtils.isNotBlank(accessToken))
        SocialSessionHolder.add(request, "access_token", state, accessToken); 
      String bindRedirectUrl = request.getParameter("bind_redirect_uri");
      if (StringUtils.isNotBlank(bindRedirectUrl))
        SocialSessionHolder.add(request, "redirect_url", state, bindRedirectUrl); 
    } 
    request.setAttribute("enableHttps", Boolean.valueOf(this.enableHttps));
    SocialAuthenticationToken token = authService.getAuthToken(request, response);
    if (token == null)
      return null; 
    Assert.notNull(token.getConnection(), "token connection is null.");
    Authentication auth = getAuthentication(request);
    if (auth == null || !auth.isAuthenticated())
      return doAuthentication(authService, request, token); 
    addConnection(authService, request, token, auth);
    return null;
  }
  
  protected Authentication doAuthentication(SocialAuthenticationService<?> authService, HttpServletRequest request, SocialAuthenticationToken token) {
    try {
      if (!authService.getConnectionCardinality().isAuthenticatePossible())
        return null; 
      token.setDetails(this.authenticationDetailsSource.buildDetails(request));
      Authentication success = getAuthenticationManager().authenticate((Authentication)token);
      Assert.isInstanceOf(UserDetails.class, success.getPrincipal(), "unexpected principle type");
      return success;
    } catch (UserUnbindException e) {
      if (this.attemptBind && this.bindUrl != null) {
        ProviderBindHelper.setConnection(request, token.getConnection());
        throw new SocialAuthenticationRedirectException(buildBindUrl(request));
      } 
      throw e;
    } 
  }
  
  protected Authentication getAuthentication(HttpServletRequest request) {
    if (callbackRequest(request)) {
      String state = request.getParameter("state");
      request.setAttribute("state", state);
      String accessToken = SocialSessionHolder.get(request, "access_token", state);
      if (StringUtils.isNotBlank(accessToken)) {
        OAuth2Authentication oAuth2Authentication = this.tokenStore.readAuthentication(accessToken);
        if (oAuth2Authentication != null) {
          oAuth2Authentication.setAuthenticated(true);
          return (Authentication)oAuth2Authentication;
        } 
      } 
    } 
    return SecurityContextHolder.getContext().getAuthentication();
  }
  
  protected void addConnection(SocialAuthenticationService<?> authService, HttpServletRequest request, SocialAuthenticationToken token, Authentication auth) {
    Object o = auth.getPrincipal();
    String userId = null;
    if (o instanceof UserDetails) {
      UserDetails self = (UserDetails)o;
      userId = self.getUsername();
    } 
    ConnectionData connectionData = token.getConnection().createData();
    if (userId == null || connectionData == null)
      return; 
    Connection<?> connection = addConnection(authService, userId, connectionData);
    String redirectUrl = SocialSessionHolder.remove(request, "redirect_url", request.getParameter("state"));
    if (redirectUrl == null)
      redirectUrl = authService.getConnectionAddedRedirectUrl(request, connection); 
    throw new SocialAuthenticationRedirectException(redirectUrl);
  }
  
  protected Connection<?> addConnection(SocialAuthenticationService<?> authService, String userId, ConnectionData data) {
    String username = this.userProviderRepository.findUsernameByProviderId(data.getProviderId(), data.getProviderUserId());
    if (username != null && StringUtils.equals(username, userId))
      throw new UserBindException("hoth.social.userAlreadyBind"); 
    if (username != null && !StringUtils.equals(username, userId))
      throw new UserBindException("hoth.social.openIdAlreadyBindOtherUser"); 
    List<SocialUserData> providerUsers = this.userProviderRepository.findProviderUser(data.getProviderId(), userId);
    if (CollectionUtils.isNotEmpty(providerUsers))
      if (providerUsers.stream().allMatch(p -> StringUtils.isBlank(p.getProviderUnionId())) || providerUsers
        .stream().anyMatch(p -> StringUtils.equals(p.getProviderUnionId(), data.getProviderUnionId())))
        throw new UserBindException("hoth.social.userAlreadyBind");  
    SocialUserData socialUserData = new SocialUserData(data);
    this.userProviderRepository.createUserBind(userId, data.getProviderId(), data.getProviderUserId(), socialUserData);
    Connection<?> connection = authService.getConnectionFactory().createConnection(data);
    connection.sync();
    return connection;
  }
  
  protected String getRequestedProviderId(HttpServletRequest request) {
    String providerId, uri = request.getRequestURI();
    uri = uri.substring(request.getContextPath().length());
    if (!uri.startsWith(this.filterProcessesUrl))
      return null; 
    uri = uri.substring(this.filterProcessesUrl.length());
    if (uri.startsWith("/")) {
      providerId = StringUtils.split(uri, "/")[0];
    } else {
      return null;
    } 
    String channel = request.getParameter("channel");
    if (StringUtils.isBlank(channel)) {
      channel = ChannelEnum.pc.name();
      this.logger.warn("Param channel is blank, use default pc channel.");
    } 
    return Provider.uniqueProviderId(providerId, channel);
  }
  
  protected boolean callbackRequest(HttpServletRequest request) {
    return (request.getRequestURI().endsWith("/callback") && request.getParameter("state") != null);
  }
  
  protected String buildBindUrl(HttpServletRequest request) {
    String returnUrl;
    if (this.bindUrl.startsWith("http://") || this.bindUrl.startsWith("https://"))
      return this.bindUrl; 
    if (!this.bindUrl.startsWith("/")) {
      returnUrl = ServletUriComponentsBuilder.fromContextPath(request).path("/" + this.bindUrl).build().toUriString();
    } else {
      returnUrl = ServletUriComponentsBuilder.fromContextPath(request).path(this.bindUrl).build().toUriString();
    } 
    if (this.enableHttps && returnUrl.startsWith("http://"))
      returnUrl = returnUrl.replace("http://", "https://"); 
    return returnUrl;
  }
}
